#!/bin/bash
## Manage linux driver patches for netmap.
##
## Initial setup: 
##
## - a git clone of netmap/linux with all the netmap-* branches created (GITDIR)
##   (also make sure that you have all the git tags in place; you may need to
##    git fetch --tags from https://github.com/torvalds/linux.git)
##
## - a directory where the linux trees for each major version of linux can 
##   be extracted (LINUX_SOURCES)
##
## - a directory containing configuration files for linux compilation
##   (LINUX_CONFIGS). This directory may be empty, and in that case
##   the linux trees will be prepared with 'make allmodconfig'
##
## - a scripts/conf file containing the above variables (GITDIR, etc.)
##   and the NETMAP_BRANCH variable, which must point to the netmap branch
##   that will be used for compilation.
##
## Once the setup is ready, the typical workflow is:
## 
## 1)  scripts/np get-range driver v1 v2
##
## This extracts the patches for driver from all the netmap-$v branches,
## with $v ranging from v1 (included) to v2 (excluded). In this phase,
## each extracted patch applies to a single kernel version. The patches
## are dropped in tmp-patches/.
##
## 2) scripts/np build-check driver
##
## Tries to build netmap+driver for each driver patch in tmp-patches,
## extracting and configuring the relevant linux kernel version. 
## Many things are cached to speed things up. Clear the 'cache' directory
## in case of trouble. Patches that fail to compile are moved to
## failed-patches. Logs are in log/patch-name.
##
## 3) scripts/np minimize driver
##
## Tries to merge the patches that survived from step 2. The resulting
## patches are put in final-patches.
##
## 4) scripts/np infty driver v2
##
## Replaces the right-end applicability of all final patches for driver
## from v2 to infty (99999).
##
## Note: 'scripts/np auto driver' will perform all steps 1-4, guessing
## the values of v1 and v2 from the available netmap-* branches.
## './scripts/np auto-all' will do the same for all known drivers.
##

PROGNAME=$0

[ -n "$1" ] || {
	scripts/help $PROGNAME;
	exit 1
}

## usage (from the dir containing the Makefile):
## 
##    scripts/np <action> [args...]
##
## where <action> is any of the functions below.
##

[ -f scripts/conf ] && source scripts/conf

NETMAP_BRANCH=${NETMAP_BRANCH:-master}
PARALLEL_MAKE=${PARALLEL_MAKE:-$(grep -c processor /proc/cpuinfo)}
NP_DEBUG=${NP_DEBUG:-0}

function error {
	echo "$@" >&2
	exit 1
}

function get-params {
	local params=$1; shift
	err_msg="$PROGNAME $COMMAND $(echo $params| perl -pe 's/\S+/<$&>/g')"
	local param
	for param in $params; do
		[[ -z "$@" ]] && error "$err_msg"
		pname=$(echo -n $param | perl -pe 's/\W/_/g')
		eval $pname="$1"
		shift
	done
	[[ -n "$@" ]] && error "$err_msg"
}

function need {
	eval "local v=\${$1}"
	[ -n "$v" -a -d "$v${2:+/$2}" ] || error "Variable $1 not set or not valid"
}

## The following enviroment variables must be set:
##
##  GITDIR: the absolute path of the netmap linux
## 	git repository, containing all the required netmap-*
## 	branches.
need GITDIR .git
##
##  LINUX_SOURCES: the absolute path of a
## 	directory used to store all required linux-* source trees
## 	(The script will extract linux-x.y.z from GITDIR if it needs
## 	it and $LINUX_SOURCES does not already contain it).
##
need LINUX_SOURCES
##  LINUX_CONFIGS: the absolute path of a
## 	directory containing the configuration files for 
## 	the linux kernel. The file for version x must be named
## 	config-x. config-all can be used as a default.
##
need LINUX_CONFIGS
## The configuration variables can be put in scripts/conf.
##

##
## Available actions:
##

##
##  driver-path <driver> <version>
##	retrieves the path of <driver> in the linux sources
##	for version <version>. The path is output to stdout.
##	It uses a local cache to minimize the expensive
##	file system search.
function driver-path()
{
	get-params "driver version" "$@"
	
	cat cache/$version/vanilla-$driver/path 2>/dev/null && return
	local kern=$(get-kernel $version)
	[ -z "$kern" ] && error "no such kernel version: $version"
	mkdir -p cache/$version/vanilla-$driver
	(
		cd $kern
		find drivers/net -name $driver
	) | tee cache/$version/vanilla-$driver/path
}


##
##  get-patch [-c] <driver> <version>
##	extract the netmap patch for the given <driver> and the
##	given kernel <version>. The patch is stored in tmp-patches
##	and the name of the patch is output to stdout.
##	If a patch with the same name already exists in tmp-patches
##	it is overwritten, unless the -c option is used,
##	in which case the existing patch is kept (the patch name is still output).
function get-patch()
{
	local use_cache
	[ "$1" = -c ] && { use_cache=1; shift; }

	get-params "driver version" "$@"

	# convert kernel version to fixed notation
	local v1=$(scripts/vers $version -c)
	# compute next kernel version (in fixed notation)
	local v2=$(scripts/vers $version -i -c)
	local patchname=vanilla--$driver--$v1--$v2
	local out=tmp-patches/$patchname
	[ -n "$use_cache" -a -s $out ] && { echo $out; return; }
	local drvpath=$(driver-path $driver $version)
	[ -n "$drvpath" ] || return
	local drvdir=$(dirname $drvpath)
	(
		cd $GITDIR
		git diff --relative=$drvdir v$version..netmap-$version -- $drvpath
	) > $out
	# an empty patch means no netmap support for this driver
	[ -s $out ] || { rm $out; return 1; }
	echo $out
	return 0;
}

##
##  get-range <driver> <version1> <version2>
##	extracts the netmap patches for the given <driver> for
##	all the kernel versions from <version1> (included) to
##	<version2> (excluded). All patches are stored in tmp-patches
##	and their names are output to stdout.
function get-range()
{
	get-params "driver version1 version2"	 "$@"

	local v=$version1
	local nv
	# while version is less than $version2
	while scripts/vers -b $v $version2 -L; do
		# compute next version
		nv=$(scripts/vers $v -i)
		if [ -z "$EXTDRV" ]; then
			get-patch $driver $v
		else
			local V1=$(scripts/vers $v -c)
			local V2=$(scripts/vers $nv -c)
			local p=tmp-patches/external--$driver--$V1--$V2
			touch $p
			echo $p
		fi
		v=$nv
	done
}


##
##  get-src <driver> <version> <dest>
##	copies the original sources of the given <driver>,
##	from the given kernel <version> to the given <dest>
##	directory.
function get-src()
{
	get-params "driver version dest" "$@"

	local kern=$(get-kernel $version)
	[ -z "$kern" ] && error "no such kernel version: $version"
	local src=$(driver-path $driver $version)
	cp -r $kern/$src $dest
}


##
##  extend <patch> <version>
##	checks wether the range of applicability of the 
##	given <patch> can be extented to include <version>.
##	It returns 0 on success and 1 on failure.
function extend()
{
	get-params "patch version" "$@"

	local _patch=$(realpath $patch)
	# extract the driver name from the patch name
	local driver=$(scripts/vers $_patch -s -p -p)
	local tmpdir1=$(mktemp -d)
	local tmpdir2=$(mktemp -d)
	trap "rm -rf $tmpdir1 $tmpdir2" 0
	# we get the driver sources for the given <version> and
	# we apply two patches separately:
	# i) the given <patch>;
	# ii) the proper patch from GITDIR.
	# We declare <patch> to be extendable if
	# - it is still applicable AND
	# - we obtain the same files from i) and ii) (ignoring whitespace)
	get-src $driver $version $tmpdir1
	get-src $driver $version $tmpdir2
	(
		cd $tmpdir1
		patch --no-backup-if-mismatch -p1 < $_patch >/dev/null 2>&1
	) || return 1
	local patch2=$(get-patch -c $driver $version)
	patch2=$(realpath $patch2)
	(
		cd $tmpdir2
		patch -p1 < $patch2 >/dev/null 2>&1
	) # this will certainly apply
	diff -qbBr $tmpdir1 $tmpdir2 >/dev/null || return 1
	return 0
} 

##
##  minimize <driver>
##	tries to minimize the number of patch files for the given
##	<driver>. It uses the patches currently found in tmp-patches
##	and stores the resulting patches in final-patches.
##	If final-patches already contained patches for <driver>,
##	they are deleted first.
function minimize()
{
	get-params "driver" "$@"

	mkdir -p final-patches
	local drv=$(basename $driver)
	local patches=$(ls tmp-patches/vanilla--$drv--* 2>/dev/null)
	[ -n "$patches" ] || return 1
	# put the patch names in $1, $2, ...
	set $patches
	rm -f final-patches/vanilla--$drv--*
	# the original patches (in tmp-patches) are ordered by version number.
	# We consider one patch in turn (the 'pivot') and try
	# to extend its range to cover the range of the next
	# patch. If this succedes, the merged patch is the new
	# pivot, otherwise the current pivot is output and the
	# next patch becomes the new pivot. The process
	# is repeated until there are no more patches to consider.
	local pivot=$1
	[ -n "$pivot" -a -e "$pivot" ] || return 1
	# extract the left end and right end of the pivot's range
	local ple=$(scripts/vers $pivot -s -p -C)
	local pre=$(scripts/vers $pivot -s -C)
	while [ -n "$pivot" ]; do
		shift
		if [ -n "$1" ]; then 
			# extract the left end and right end of the next patch
			local nle=$(scripts/vers $1 -s -p -C)
			local nre=$(scripts/vers $1 -s -C)
			# we admit no gaps in the range
			if [ $pre = $nle ] && extend $pivot $nle; then
				pre=$nre
				continue
			fi
		fi
		# either out of patches or failed merge.
		# Compute the file name of the current pivot and store
		# the patch in its final location
		out=$(scripts/vers vanilla $drv $ple -c $pre -c -S4)
		cp $pivot final-patches/$out
		# the new pivot becames the next patch (if any)
		pivot=$1
		pre=$nre
		ple=$nle
	done
	return 0
}

##
##  infty <driver> <version>
##	if final-patches contains a patch for <driver> with a range
##	ending in <version>, extend it to infinity.
##	Do nothing otherwise.
function infty()
{
	get-params "driver version" "$@"

	local drv=$(basename $driver)
	# convert kernel version to fixed notation
	local v=$(scripts/vers $version -c)
	local last=$(ls final-patches/vanilla--$drv--*--$v 2>/dev/null|tail -n1)
	[ -n "$last" ] || return 1
	mv -n $last $(scripts/vers $last -s -p 99999 -S4) 2>/dev/null
}

function get-kernel()
{
	get-params "version" "$@"

	local dst="$(realpath $LINUX_SOURCES)/linux-$version"

	[ -d $dst ] && { echo $dst; return; }

	local v=$version

	(
		cd $GITDIR
		if git show-ref --tags --quiet --verify -- "refs/tags/v$v"; then
			mkdir -p $dst
			git archive v$v | tar xf - -C $dst
			echo $dst
		fi
	)
}


##
##  build-prep <version>
##	prepare the linux tree for <version> to be ready
##	for external modules compilation.
##	The tree is put in $LINUX_SOURCES/linux-<version> and the
##	configuration is obtained from $LINUX_CONFIGS/config-<version>,
##	if available, otherwise $LINUX_CONFIGS/config-all is used
##	as a default; if also the latter does not exist, the kernel is
##	configure using 'allmodconfig'.
##	Errors are logged to $LINUX_CONFIGS/linux-<version>.log.
##	If $LINUX_SOURCES/linux-<version>/.build-prep already exists,
##	nothing is done.
##	In all cases, the absolute path of linux-<version> is
##	output.
function build-prep()
{
	get-params "version" "$@"

	local dst=$(get-kernel $version)
	local last

	[ -f $dst/.build-prep ] && { echo $dst; return; }

	(
		cd $dst
		last=compiler-gcc.h
		for i in $(seq 9); do
			[ -e include/linux/compiler-gcc$i.h ] ||
				ln -s $last include/linux/compiler-gcc$i.h
			last=compiler-gcc$i.h
		done
		# force disabling PIE and fcf-protection
		sed -i -e '/^all: vmlinux/a\
\
KBUILD_CFLAGS += $(call cc-option, -fno-pie)\
KBUILD_CFLAGS += $(call cc-option, -no-pie)\
KBUILD_CFLAGS += $(call cc-option, -fcf-protection=none)\
KBUILD_AFLAGS += $(call cc-option, -fno-pie)\
KBUILD_CPPFLAGS += $(call cc-option, -fno-pie)' Makefile
		if [ -f $LINUX_CONFIGS/config-$version ]; then
			cp $LINUX_CONFIGS/config-$version .config
			yes '' | make oldconfig
		else
			make allmodconfig
		fi
		# old kernels' selinux causes compilation failures with gcc >= 9
		echo "CONFIG_SECURITY_SELINUX=n" >> .config
		yes '' | make oldconfig
		# some tools do not compile with -Werror and gcc >= 9
		sed -i 's/-Werror/-Wno-error/g' $(grep -Rl -- -Werror tools)
		make modules_prepare
		touch .build-prep
	) >$dst.log 2>&1 || error "build-prep failed for linux $version. Please check $dst.log"
	echo $dst
}

##
##  check-patch <patch>
##	check that the given <patch> applies and compiles without
##	error for all its declared range of applicability.
##	Errors are logged to log/<patch>.
function check-patch()
{
	get-params "patch" "$@"

	local _patch=$(basename $patch)
	# extract the left version
	local v1=$(scripts/vers $_patch -s -p -C)
	# extract the right version
	local v2=$(scripts/vers $_patch -s -C)
	# extract the uncoverted right version (might be 99999)
	local end=$(scripts/vers $_patch -s)
	# extract the driver name
	local driver=$(scripts/vers $_patch -s -p -p)
	# possibly extract the selected driver version
	driver_version=$(echo "$driver" | sed -n 's/^.*://p')
	driver=${driver%%:*}
	# extract the driver type (vanilla or external)
	local dtype=$(scripts/vers $_patch -s -p -p -p)
	local p=$(realpath $patch)
	mkdir -p log
	local log="$(realpath log)/$(basename $patch)"
	local nmcommit=$(cd ..; git show-ref -s heads/$NETMAP_BRANCH)
	local warn=false

	rm -f $log

	echo -n $patch...

	while scripts/vers -b $v1 $v2 -L; do
		# cache lookup
		local cache=$PWD/cache/$v1/$dtype-$driver${driver_version+:$driver_version}
		mkdir -p $cache
		local cpatch=$cache/patch
		local cnmcommit=$cache/nmcommit
		local cstatus=$cache/status
		local cwarn=$cache/warn
		local clog=$cache/log
		if [ -f $cpatch ]				&&
		   cmp -s $cpatch $patch			&&
		   [ "$nmcommit" = "$(cat $cnmcommit)" ]; then
			cp $clog $log
			ok=$(cat $cstatus)
			if [ -f "$cwarn" ]; then warn=$(cat $cwarn); fi
		else
			# update cache
			cp $patch $cpatch
			echo $nmcommit > $cnmcommit

			local ksrc=$(build-prep $v1)
			if [ -z "$ksrc" ]; then
				rm -rf $cache
				if [ "$end" = 99999 ]; then
					ok=true
					break
				fi
				error "no such kernel version: $v1"
			fi
			local tmpdir=$(mktemp -d)
			if [ "$NP_DEBUG" -lt 1 ]; then
				trap "rm -rf $tmpdir" 0
			fi
			echo "====== $tmpdir =====" >>$log
			(cd ..; git archive $NETMAP_BRANCH | tar xf - -C $tmpdir )
			pushd $tmpdir/LINUX >/dev/null
			rm -f patches
			ok=false
			config_opts=
			if [ -z "$EXT_DRIVERS" ]; then
				config_opts=--no-ext-drivers
				mkdir single-patch
				ln -s single-patch patches
				cp $p single-patch
			else
				rm -rf ext-drivers
				ln -s $EXT_DRIVERS ext-drivers
				ln -s final-patches patches
				if [ -n "$driver_version" ]; then
					config_opts="$config_opts --select-version=$driver:$driver_version"
				fi
			fi
			if [ "$driver" != "veth.c" ]; then
				config_opts="$config_opts --disable-pipe"
			fi
			./configure --kernel-dir=$ksrc --driver-suffix=$DRIVER_SUFFIX \
				--drivers=$driver \
				--disable-generic \
				--disable-vale \
				--disable-monitor \
				--disable-ptnetmap \
				--no-apps \
				--kernel-opts=CONFIG_STACK_VALIDATION= \
				$config_opts \
				--cache=$cache >>$log
			(make get-$driver && make -j $PARALLEL_MAKE) >>$log 2>&1 && ok=true
			grep -q warning: $log && { warn=true; echo $warn > $cwarn; }
			cat config.log >>$log
			cat netmap_linux_config.h >>$log 2>/dev/null
			popd >/dev/null
			cp $log $clog
		fi
		[ "$ok" = true ] || { echo FAILED; echo false > $cstatus; return 1; }
		echo true > $cstatus
		if [ "$NP_DEBUG" -lt 2 ]; then
			rm -rf $tmpdir
		fi
		# compute next version
		v1=$(scripts/vers $v1 -i)
	done
	if [ "$warn" = true ]; then
		echo WARNING
	else
		echo OK
	fi
}

##
##  build-check <driver>
##	do a check-patch for all the patches of <driver> that are
##	currently in tmp-patches. Patches that fail the check
##	are moved to failed-patches.
function build-check()
{
	get-params "driver" "$@"

	mkdir -p failed-patches
	local dtype=vanilla
	if [ "$EXTDRV" = 1 ]; then
		dtype=external
	fi
	local drv=$(basename $driver)
	local patches=$(ls tmp-patches/$dtype--$drv--* 2>/dev/null)
	local p
	for p in $patches; do
		check-patch $p || mv $p failed-patches
	done
}

MINVERS=2.6.32
MAXVERS=
NEXTVERS=
##
##  auto <driver>
##	do all the steps of the typical workflow for <driver>.
##	The linux versions are obtained automatically from the
##	available netmap-* branches in GITDIR
function auto()
{
	get-params "driver" "$@"

	if [ -z "$MAXVERS" ]; then 
		# get the latest netmap-* branch
		local branches=$(
			cd $GITDIR;
			git for-each-ref refs/heads/netmap-[0-9]* --format='%(refname:short)'
		)
		MAXVERS=$MINVERS
		local b
		for b in $branches; do
			local v=${b#netmap-}
			if scripts/vers -b $MAXVERS $v -L; then
				MAXVERS=$v
			fi
		done
		echo "Latest netmap linux branch: $MAXVERS"
		NEXTVERS=$(scripts/vers $MAXVERS -i)
	fi
	get-range $driver $MINVERS $NEXTVERS
	build-check $driver
	if [ -z "$EXTDRV" ]; then
		minimize $driver
		infty $driver $NEXTVERS
	fi
}

##
##  forall <cmd> [args...]
##	exec <cmd> <driver> [args...] for all known drivers.
function forall()
{
	local cmd=$1
	shift

	local driver
	for driver in $(./configure --show${EXTDRV:+-ext}-drivers); do
		$cmd $(basename $driver) "$@"
	done
}

mkdir -p tmp-patches

EXTDRV=
if [ "$1" = -e ]; then
	if [ -z "$EXT_DRIVERS" ]; then
		echo "EXT_DRIVERS not set"
		exit 1
	fi
	if ! [ -d "$EXT_DRIVERS" ]; then
		echo "$EXT_DRIVERS not a directory"
		exit 1
	fi
	EXTDRV=1
	shift
else
	EXT_DRIVERS=
fi
COMMAND=$1; shift
case $COMMAND in
##
##  <cmd>-all [args...]
##	same as: forall <cmd> [args...]
*-all)
	forall ${COMMAND%-all} "$@"
	;;
-[hH]|--help|-help|help)
	scripts/help $PROGNAME
	;;
*)
	$COMMAND "$@"
	;;
esac
